using System;
using System.Collections;
using System.Drawing;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using DirectShowLib;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace MultiVMR9cs
{
    /// <summary>
    /// Derived from the VMR9Allocator2 sample in DirectShowLib
    /// </summary>
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    internal class Allocator : IVMRSurfaceAllocator9, IVMRImagePresenter9, IDisposable
    {
        #region Fields

        // Constants
        private const int E_FAIL = unchecked((int)0x80004005);
        private const int D3DERR_INVALIDCALL = unchecked((int)0x8876086c);
        private const int DxMagicNumber = -759872593;

        // the DirectX device given to this allocator
        private Device _device = null;

        private AdapterInformation _adapterInfo = null;
        private DeviceCreationParameters _creationParameters;

        // the surface that is given to the VMR9 by the allocator
        private Surface _videoSurface = null;
        private IntPtr _unmanagedSurface = IntPtr.Zero;
 
        // texture that is given to the VMR9 in non-yuv mixing mode
        private Texture _RGBvideoTexture = null;

        // the surface and texture that we will copy to when PresentImage is called
        private Texture _privateTexture = null;
        private Surface _privateSurface = null;

        // flag that indicates the Alocator should stretch the video onto the private texture to keep it's aspect ratio
        private bool _keepAspectRatio = true;

        // the size of the Private Texture
        private Size _textureSize;

        // the actual resolution of the Video
        private Size _videoSize;

        // color for letterboxing
        private Color _borderColor = Color.FromArgb(0, Color.Black);

        // the size of the off-screen surface (may be different on cards that don't support non-pow2 textures)
        private Size _surfaceSize;

        // if keeping the aspect ratio, the offset to copy the video surface onto the private texture
        private Point _resizeOffset;

        // the start and end times for samples delivered by PresentImage.
        private long _sampleStartTime;
        private long _sampleEndTime;

        private IVMRSurfaceAllocatorNotify9 _vmrSurfaceAllocatorNotify = null;

        // state flags
        private bool _disposed = false;
        private bool _isInit = false;
        private bool _deviceChanged = false;
        private bool _clearTexture = false;
        private bool _yuvMixingMode;

        private object _lockObject = new object();

        #endregion

        #region ctor and destructor

        public Allocator(Device dev,bool YUVMixingMode)
        {
            _yuvMixingMode = YUVMixingMode;
            _adapterInfo = Manager.Adapters.Default;
            _device = dev;
            _creationParameters = _device.CreationParameters;
        }

        ~Allocator()
        {
            Dispose(false);
        }

        #endregion

        #region Properties

        public Device Device
        {
            get
            {
                return _device;
            }
            internal set
            {
                _deviceChanged = true;
                _device = value;
                _creationParameters = _device.CreationParameters;
                _adapterInfo = Manager.Adapters[_creationParameters.AdapterOrdinal];
            }
        }

        public Color BorderColor
        {
            get
            {
                return _borderColor;
            }
            set
            {
                _borderColor = value;

                // indicate we need to clear the texture on the next PresentImage
                _clearTexture = true;
            }
        }

        // Handy Properties to retrieve the texture, its size and the video size
        public Texture Texture
        {
            get
            {
                return _privateTexture;
            }
        }

        public Size TextureSize
        {
            get
            {
                return _textureSize;
            }
        }

        public Size VideoSize
        {
            get
            {
                return _videoSize;
            }
        }

        public long SampleStartTime
        {
            get
            {
                return _sampleStartTime;
            }
        }

        public long SampleEndTime
        {
            get
            {
                return _sampleEndTime;
            }
        }

        public bool KeepAspectRatio
        {
            get
            {
                return _keepAspectRatio;
            }
            set
            {
                if (value != _keepAspectRatio)
                {
                    lock (_lockObject)
                    {
                        _keepAspectRatio = value;

                        if(_privateSurface != null)
                        {
                            _privateSurface.Dispose();
                            _privateSurface = null;
                        }

                        if (_privateTexture != null)
                        {
                            _privateTexture.Dispose();
                            _privateTexture = null;
                        }

                        CreatePrivateTexture();
                    }
                }
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Delete allocated surfaces
        /// </summary>
        internal void DeleteSurfaces()
        {
            lock (_lockObject)
            {
                if (_privateTexture != null)
                {
                    _privateTexture.Dispose();
                    _privateTexture = null;
                }

                if (_privateSurface != null)
                {
                    _privateSurface.Dispose();
                    _privateSurface = null;
                }

                if (_videoSurface != null)
                {
                    _videoSurface.Dispose();
                    _videoSurface = null;
                }

                if (_unmanagedSurface != IntPtr.Zero)
                {
                    int refc = Marshal.Release(_unmanagedSurface);                    
                    _unmanagedSurface = IntPtr.Zero;
                }

                if (_RGBvideoTexture != null)
                {
                    _RGBvideoTexture.Dispose();
                    _RGBvideoTexture = null;
                }
            }
        }

        /// <summary>
        /// Helper method to convert interger FourCC into a readable string
        /// </summary>
        /// <param name="fcc"></param>
        /// <returns></returns>
        private string FourCCToStr(int fcc)
        {
            byte[] chars = new byte[4];

            if (fcc < 100)
                return fcc.ToString();

            for (int i = 0; i < 4; i++)
            {
                chars[i] = (byte)(fcc & 0xff);
                fcc = fcc >> 8;
            }

            return System.Text.Encoding.ASCII.GetString(chars);
        }

        /// <summary>
        /// Helper method to create the private texture the allocator copies to in PresentImage
        /// </summary>
        private void CreatePrivateTexture()
        {
            _textureSize = new Size(_surfaceSize.Width, _surfaceSize.Height);

            if (_keepAspectRatio)
            {
                // change the aspect ratio of the private texture to match the aspect ratio of the device
                float deviceAR = (float)_device.PresentationParameters.BackBufferWidth / _device.PresentationParameters.BackBufferHeight;
                float videoAR = (float)_surfaceSize.Width / (float)_surfaceSize.Height;

                if (deviceAR != videoAR)
                {
                    // make it taller
                    _textureSize.Height = (int)((float)_textureSize.Width / deviceAR);
                    _resizeOffset = new Point(0, (int)((_textureSize.Height - _surfaceSize.Height) / 2));
                    if (_textureSize.Height < _surfaceSize.Height)
                    {
                        // Not what we were looking for, make it wider.
                        _textureSize = new Size(_surfaceSize.Width, _surfaceSize.Height);
                        _textureSize.Width = (int)((float)_textureSize.Height * deviceAR);
                        _resizeOffset = new Point((int)((_textureSize.Width - _surfaceSize.Width) / 2), 0);
                    }
                }
            }
            else
            {
                _resizeOffset = new Point(0, 0);
            }

            // Create a private texture for the client application
            _privateTexture = new Texture(
              _device,
              _textureSize.Width,
              _textureSize.Height,
              1,
              Usage.RenderTarget,
              Format.X8R8G8B8,
              Pool.Default
              );

            // Get the MipMap surface 0 for the copy (see PresentImage)
            _privateSurface = _privateTexture.GetSurfaceLevel(0);

            if (_keepAspectRatio)
            {
                // clear the private texture to border color
                ClearPrivateTexture();
            }
        }

        private void ClearPrivateTexture()
        {
            // We'll create a small surface from a color and use that to strectrect into the texture.
            // This is a work-around for some drivers having problems doing device.clear outside BeginScene/EndScene

            Bitmap tempB = new Bitmap(2, 2); // Surface.FromBitmap requires pow2 bitmaps.
            Graphics g = Graphics.FromImage(tempB);
            g.Clear(_borderColor);
            g.Dispose();
            Surface temps = Surface.FromBitmap(_device, tempB, Pool.Default);
            tempB.Dispose();
            _device.StretchRectangle(temps, new Rectangle(0, 0, 2, 2), _privateSurface, new Rectangle(0, 0, _textureSize.Width, _textureSize.Height), TextureFilter.Point);
            temps.Dispose();
        }

        #endregion

        #region IDisposable Impl

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                DeleteSurfaces();
                _disposed = true;
            }
        }

        #endregion

        #region IVMRSurfaceAllocator9 Impl

        public int InitializeDevice(IntPtr dwUserID, ref VMR9AllocationInfo lpAllocInfo, ref int lpNumBuffers)
        {
            int hr = 0;

            Debug.WriteLine(string.Format("{0}x{1} : {2} / {3} / 0x{4:x}", lpAllocInfo.dwWidth, lpAllocInfo.dwHeight, FourCCToStr(lpAllocInfo.Format), lpAllocInfo.Pool, lpAllocInfo.dwFlags));
            Format tempf = (Format)lpAllocInfo.Format;

            // if format is YUV ? (note : 0x30303030 = "    ")
            if (lpAllocInfo.Format > 0x30303030)
            {
                // Check if the hardware supports format conversion from this YUV format to the RGB format
                if (!Manager.CheckDeviceFormatConversion(_creationParameters.AdapterOrdinal, _creationParameters.DeviceType, (Format)lpAllocInfo.Format, _adapterInfo.CurrentDisplayMode.Format))
                {
                    // If not, refuse this format!
                    // The VMR9 will propose other formats supported by the downstream filter output pin.
                    return D3DERR_INVALIDCALL;
                }
            }

            try
            {
                IntPtr unmanagedDevice = _device.GetObjectByValue(DxMagicNumber);
                IntPtr hMonitor = Manager.GetAdapterMonitor(_adapterInfo.Adapter);

                // Give our Direct3D device to the VMR9 filter if it has not been initialized yet
                if (!_isInit)
                {
                    hr = _vmrSurfaceAllocatorNotify.SetD3DDevice(unmanagedDevice, hMonitor);
                    DsError.ThrowExceptionForHR(hr);
                }
                else if(_deviceChanged)
                {
                    hr = _vmrSurfaceAllocatorNotify.ChangeD3DDevice(unmanagedDevice, hMonitor);
                    DsError.ThrowExceptionForHR(hr);
                    _deviceChanged = false;
                }

                _videoSize = new Size(lpAllocInfo.dwWidth, lpAllocInfo.dwHeight);

                int width = 1;
                int height = 1;

                // See if card has unconditional support for non-pow-2 textures
                if (_device.DeviceCaps.TextureCaps.SupportsPower2 || _device.DeviceCaps.TextureCaps.SupportsNonPower2Conditional) 
                {
                    // Compute the ideal size
                    while (width < lpAllocInfo.dwWidth)
                        width = width << 1;
                    while (height < lpAllocInfo.dwHeight)
                        height = height << 1;

                    // notify this change to the VMR9 filter
                    lpAllocInfo.dwWidth = width;
                    lpAllocInfo.dwHeight = height;
                }

                _surfaceSize = new Size(lpAllocInfo.dwWidth, lpAllocInfo.dwHeight);

                // Just in case
                DeleteSurfaces();

                CreatePrivateTexture();

                lpAllocInfo.dwFlags |= VMR9SurfaceAllocationFlags.OffscreenSurface;

                // An offscreen surface must be created
                _videoSurface = _device.CreateOffscreenPlainSurface(lpAllocInfo.dwWidth, lpAllocInfo.dwHeight, (Format)lpAllocInfo.Format, (Pool)lpAllocInfo.Pool);

                // And get the unmanaged pointer
                _unmanagedSurface = _videoSurface.GetObjectByValue(DxMagicNumber);

                // This allocator only supports 1 buffer.
                // Notify the VMR9 filter
                lpNumBuffers = 1;
            }
            catch (DirectXException e)
            {
                // A Direct3D error can occure : Notify it to the VMR9 filter
                return e.ErrorCode;
            }
            catch
            {
                // Or else, notify a more general error
                return E_FAIL;
            }

            // indicate the Device has been initialized
            _isInit = true;

            // This allocation is a success
            return 0;
        }

        public int TerminateDevice(IntPtr dwID)
        {
            DeleteSurfaces();
            return 0;
        }

        public int GetSurface(IntPtr dwUserID, int SurfaceIndex, int SurfaceFlags, out IntPtr lplpSurface)
        {
            lplpSurface = IntPtr.Zero;

            // If the filter ask for an invalid buffer index, return an error.
            if (SurfaceIndex >= 1)
                return E_FAIL;

            lock (_lockObject)
            {
                if (_device.CheckCooperativeLevel())
                {
                    // IVMRSurfaceAllocator9.GetSurface documentation states that the caller releases the returned 
                    // interface, so we must increment its reference count.
                    lplpSurface = _unmanagedSurface;
                    Marshal.AddRef(lplpSurface);
                    return 0;
                }
                else
                {
                    // the device is in the process of being reset so you get NOTHING! NATHA! ZIP!
                    lplpSurface = IntPtr.Zero;
                    return 0;
                }
            }
        }

        public int AdviseNotify(IVMRSurfaceAllocatorNotify9 lpIVMRSurfAllocNotify)
        {
            lock (_lockObject)
            {
                _vmrSurfaceAllocatorNotify = lpIVMRSurfAllocNotify;
                // Give our Direct3D device to the VMR9 filter
                IntPtr unmanagedDevice = _device.GetObjectByValue(DxMagicNumber);
                IntPtr hMonitor = Manager.GetAdapterMonitor(_creationParameters.AdapterOrdinal);
                return _vmrSurfaceAllocatorNotify.SetD3DDevice(unmanagedDevice, hMonitor);
            }
        }

        #endregion

        #region IVMRImagePresenter9 Impl

        public int StartPresenting(IntPtr dwUserID)
        {
            lock (_lockObject)
            {
                if (_device == null)
                    return E_FAIL;

                return 0;
            }
        }

        public int StopPresenting(IntPtr dwUserID)
        {
            return 0;
        }

        public int PresentImage(IntPtr dwUserID, ref VMR9PresentationInfo lpPresInfo)
        {
            lock (_lockObject)
            {
                try
                {
                    if (_clearTexture)
                    {
                        ClearPrivateTexture();
                        _clearTexture = false;
                    }

                    // Use StretchRectangle to do the Pixel Format conversion and/or resizing
                    _device.StretchRectangle(
                      _videoSurface,
                      new Rectangle(Point.Empty, _videoSize),
                      _privateSurface,
                      new Rectangle(_resizeOffset, _videoSize),
                      TextureFilter.None
                      );
                }
                catch (DirectXException e)
                {
                    // A Direct3D error can occur : Notify it to the VMR9 filter
                    return e.ErrorCode;
                }
                catch
                {
                    // Or else, notify a more general error
                    return E_FAIL;
                }

                _sampleStartTime = lpPresInfo.rtStart;
                _sampleEndTime = lpPresInfo.rtEnd;

                // This presentation is a success
                return 0;
            }
        }
        #endregion
    }
}
